﻿// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

//using System.Windows.Threading;
//using ICSharpCode.ILSpy.Options;
using Mono.Cecil;
using System;
using System.IO;
using System.Threading.Tasks;

namespace DevelopTool.Common.ILSpy
{
    /// <summary>
    /// Represents an assembly loaded into ILSpy.
    /// </summary>
    public sealed class LoadedAssembly
    {
        private readonly Task<ModuleDefinition> assemblyTask;
        private readonly AssemblyList assemblyList;
        private readonly string fileName;
        private readonly string shortName;
        public Guid ProjectGuid { get; set; }
        public string ProjectFileName { get; set; }

        public LoadedAssembly(AssemblyList assemblyList, string fileName, Stream stream = null)
        {
            if (assemblyList == null)
                throw new ArgumentNullException("assemblyList");
            if (fileName == null)
                throw new ArgumentNullException("fileName");
            this.assemblyList = assemblyList;
            this.fileName = fileName;

            this.assemblyTask = Task.Factory.StartNew<ModuleDefinition>(LoadAssembly, stream); // requires that this.fileName is set
            this.shortName = Path.GetFileNameWithoutExtension(fileName);
        }

        /// <summary>
        /// Gets the Cecil ModuleDefinition.
        /// Can be null when there was a load error.
        /// </summary>
        public ModuleDefinition ModuleDefinition
        {
            get
            {
                try
                {
                    return assemblyTask.Result;
                }
                catch (AggregateException)
                {
                    return null;
                }
            }
        }

        /// <summary>
        /// Gets the Cecil AssemblyDefinition.
        /// Is null when there was a load error; or when opening a netmodule.
        /// </summary>
        public AssemblyDefinition AssemblyDefinition
        {
            get
            {
                var module = this.ModuleDefinition;
                return module != null ? module.Assembly : null;
            }
        }

        public AssemblyList AssemblyList
        {
            get { return assemblyList; }
        }

        public string FileName
        {
            get { return fileName; }
        }

        public string ShortName
        {
            get { return shortName; }
        }

        public string Text
        {
            get
            {
                if (AssemblyDefinition != null)
                {
                    return String.Format("{0} ({1})", ShortName, AssemblyDefinition.Name.Version);
                }
                else
                {
                    return ShortName;
                }
            }
        }

        public bool IsLoaded
        {
            get { return assemblyTask.IsCompleted; }
        }

        public bool HasLoadError
        {
            get { return assemblyTask.IsFaulted; }
        }

        public bool IsAutoLoaded { get; set; }

        private ModuleDefinition LoadAssembly(object state)
        {
            var stream = state as Stream;
            ModuleDefinition module;

            // runs on background thread
            ReaderParameters p = new ReaderParameters();
            p.AssemblyResolver = new MyAssemblyResolver(this);

            if (stream != null)
            {
                // Read the module from a precrafted stream
                module = ModuleDefinition.ReadModule(stream, p);
            }
            else
            {
                // Read the module from disk (by default)
                try
                {
                    module = ModuleDefinition.ReadModule(fileName, p);
                }
                catch
                {
                    return null;
                }
            }

            //if (DecompilerSettingsPanel.CurrentDecompilerSettings.UseDebugSymbols) {
            //				try {
            //					LoadSymbols(module);
            //				} catch (IOException) {
            //				} catch (UnauthorizedAccessException) {
            //				} catch (InvalidOperationException) {
            //					// ignore any errors during symbol loading
            //				}
            //}
            return module;
        }

        //		private void LoadSymbols(ModuleDefinition module)
        //		{
        //			// search for pdb in same directory as dll
        //			string pdbName = Path.Combine(Path.GetDirectoryName(fileName), Path.GetFileNameWithoutExtension(fileName) + ".pdb");
        //			if (File.Exists(pdbName)) {
        //				using (Stream s = File.OpenRead(pdbName)) {
        //					module.ReadSymbols(new Mono.Cecil.Pdb.PdbReaderProvider().GetSymbolReader(module, s));
        //				}
        //				return;
        //			}
        //
        //			// TODO: use symbol cache, get symbols from microsoft
        //		}

        [ThreadStatic]
        private static int assemblyLoadDisableCount;

        public static IDisposable DisableAssemblyLoad()
        {
            assemblyLoadDisableCount++;
            return new DecrementAssemblyLoadDisableCount();
        }

        private sealed class DecrementAssemblyLoadDisableCount : IDisposable
        {
            private bool disposed;

            public void Dispose()
            {
                if (!disposed)
                {
                    disposed = true;
                    assemblyLoadDisableCount--;
                    // clear the lookup cache since we might have stored the lookups failed due to DisableAssemblyLoad()
                    //MainWindow.Instance.CurrentAssemblyList.ClearCache();
                }
            }
        }

        private sealed class MyAssemblyResolver : IAssemblyResolver
        {
            private readonly LoadedAssembly parent;

            public MyAssemblyResolver(LoadedAssembly parent)
            {
                this.parent = parent;
            }

            public AssemblyDefinition Resolve(AssemblyNameReference name)
            {
                var node = parent.LookupReferencedAssembly(name);
                return node != null ? node.AssemblyDefinition : null;
            }

            public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
            {
                var node = parent.LookupReferencedAssembly(name);
                return node != null ? node.AssemblyDefinition : null;
            }

            public AssemblyDefinition Resolve(string fullName)
            {
                var node = parent.LookupReferencedAssembly(fullName);
                return node != null ? node.AssemblyDefinition : null;
            }

            public AssemblyDefinition Resolve(string fullName, ReaderParameters parameters)
            {
                var node = parent.LookupReferencedAssembly(fullName);
                return node != null ? node.AssemblyDefinition : null;
            }
        }

        public IAssemblyResolver GetAssemblyResolver()
        {
            return new MyAssemblyResolver(this);
        }

        public LoadedAssembly LookupReferencedAssembly(AssemblyNameReference name)
        {
            if (name == null)
                throw new ArgumentNullException("name");
            if (name.IsWindowsRuntime)
            {
                return assemblyList.winRTMetadataLookupCache.GetOrAdd(name.Name, LookupWinRTMetadata);
            }
            else
            {
                return assemblyList.assemblyLookupCache.GetOrAdd(name.FullName, LookupReferencedAssemblyInternal);
            }
        }

        public LoadedAssembly LookupReferencedAssembly(string fullName)
        {
            return assemblyList.assemblyLookupCache.GetOrAdd(fullName, LookupReferencedAssemblyInternal);
        }

        private LoadedAssembly LookupReferencedAssemblyInternal(string fullName)
        {
            foreach (LoadedAssembly asm in assemblyList.GetAssemblies())
            {
                if (asm.AssemblyDefinition != null && fullName.Equals(asm.AssemblyDefinition.FullName, StringComparison.OrdinalIgnoreCase))
                    return asm;
            }
            if (assemblyLoadDisableCount > 0)
                return null;

            //			if (!App.Current.Dispatcher.CheckAccess()) {
            //				// Call this method on the GUI thread.
            //				return (LoadedAssembly)App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Func<string, LoadedAssembly>(LookupReferencedAssembly), fullName);
            //			}

            var name = AssemblyNameReference.Parse(fullName);
            string file = null; // GacInterop.FindAssemblyInNetGac(name);
            if (file == null)
            {
                string dir = Path.GetDirectoryName(this.fileName);
                if (File.Exists(Path.Combine(dir, name.Name + ".dll")))
                    file = Path.Combine(dir, name.Name + ".dll");
                else if (File.Exists(Path.Combine(dir, name.Name + ".exe")))
                    file = Path.Combine(dir, name.Name + ".exe");
            }
            if (file != null)
            {
                var loaded = assemblyList.OpenAssembly(file, true);
                return loaded;
            }
            else
            {
                return null;
            }
        }

        private LoadedAssembly LookupWinRTMetadata(string name)
        {
            foreach (LoadedAssembly asm in assemblyList.GetAssemblies())
            {
                if (asm.AssemblyDefinition != null && name.Equals(asm.AssemblyDefinition.Name.Name, StringComparison.OrdinalIgnoreCase))
                    return asm;
            }
            if (assemblyLoadDisableCount > 0)
                return null;
            //			if (!App.Current.Dispatcher.CheckAccess()) {
            //				// Call this method on the GUI thread.
            //				return (LoadedAssembly)App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Func<string, LoadedAssembly>(LookupWinRTMetadata), name);
            //			}

            string file = Path.Combine(Environment.SystemDirectory, "WinMetadata", name + ".winmd");
            if (File.Exists(file))
            {
                return assemblyList.OpenAssembly(file, true);
            }
            else
            {
                return null;
            }
        }

        public Task ContinueWhenLoaded(Action<Task<ModuleDefinition>> onAssemblyLoaded, TaskScheduler taskScheduler)
        {
            return this.assemblyTask.ContinueWith(onAssemblyLoaded, taskScheduler);
        }

        /// <summary>
        /// Wait until the assembly is loaded.
        /// Throws an AggregateException when loading the assembly fails.
        /// </summary>
        public void WaitUntilLoaded()
        {
            assemblyTask.Wait();
        }
    }
}